style: visual redesign — theme tokenization, WCAG fixes, PlotOfTheDay redesign#5235
style: visual redesign — theme tokenization, WCAG fixes, PlotOfTheDay redesign#5235MarkusNeusinger merged 6 commits intomainfrom
Conversation
…aner navigation - Theme tokens: font scale +15%, semantic color tokens for WCAG AA contrast - MUI theme: primary color aligned to Python blue, global tooltip style - Navigation: remove "catalog" from breadcrumb, add catalog link in rightAction + footer - Lightbox: new ImageLightbox component replaces image-click-to-go-back with zoom - SpecPage: keyboard arrow keys for library switching, "< all implementations" link - SpecOverview: responsive grid fix (1/2/3 cols instead of fixed 3) - RelatedSpecs: single-row auto-fit grid, abbreviated library names, tooltips - Tags: larger chips (24px), tag count tooltips from globalCounts API - Cards: focus-visible only, ">>> copied" overlay, blue hover on action buttons - Toolbar: larger icons (1.4rem), better contrast - Layout: ultrawide support (max-width 2200px, xl padding) - All components migrated from hardcoded values to theme tokens - New style guide: docs/reference/style-guide.md Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- increase icon sizes for better visibility - adjust related specs query limit to 24 - enhance tab navigation in RelatedSpecs component - remove unnecessary elements from UI for cleaner design
…n PlotOfTheDay - Replace 80+ hardcoded hex colors with theme tokens across all components and pages - Fix 15+ WCAG AA contrast violations (#9ca3af used as text color) - Add new theme tokens: fontSize.micro/xxs, colors.primaryDark/accentBg/codeBlock/highlight/tooltipLight - Add shared style constants: headingStyle, subheadingStyle, textStyle, codeBlockStyle, tableStyle - Centralize LIB_ABBREV map in constants, add responsive shortLabel to Breadcrumb (pp/mpl/sns on mobile) - Redesign PlotOfTheDay: terminal-style frame, split layout, fade-in animation, GitHub link, real versions - Fix 3 pre-existing lint errors (setState in useEffect → render-time ref pattern) - Fix SpecPage useCallback missing dependency warning - Remove unused ImageLightbox component - Improve RelatedSpecs grid (2→3→4→6 columns), spacing, "tags in common" label - Update style guide documentation with all new tokens and sections - Update Serena memories with style guide reference Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR implements a frontend visual redesign by centralizing theme values into reusable tokens, addressing WCAG contrast issues, and updating several UI components (notably PlotOfTheDay and breadcrumb behavior). It also extends the API payload for PlotOfTheDay and increases the maximum “related” results returned.
Changes:
- Introduce/expand theme tokens and shared
sxstyle constants; replace hardcoded colors/fonts across many pages/components. - Redesign PlotOfTheDay UI and enrich it with GitHub source links + library/Python version metadata from the API.
- Add responsive breadcrumb short labels and refresh several pages (Catalog/Spec/Stats/Legal/MCP/etc.) to match the new style guide.
Reviewed changes
Copilot reviewed 34 out of 35 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| docs/reference/style-guide.md | New/expanded frontend style guide documentation |
| app/src/theme/index.ts | Adds new tokens + shared style constants |
| app/src/pages/StatsPage.tsx | Refactors styling to tokens + breadcrumb shortLabel |
| app/src/pages/SpecPage.tsx | Download flow update, keyboard nav, breadcrumb/action redesign |
| app/src/pages/NotFoundPage.tsx | Tokenizes fonts/colors |
| app/src/pages/McpPage.tsx | Reuses shared theme constants (heading/text/table/code) |
| app/src/pages/LegalPage.tsx | Reuses shared theme constants + breadcrumb shortLabel |
| app/src/pages/InteractivePage.tsx | Tokenizes background/typography + breadcrumb shortLabel |
| app/src/pages/HomePage.tsx | Tokenizes scroll-to-top button colors |
| app/src/pages/DebugPage.tsx | Tokenizes UI + breadcrumb shortLabel |
| app/src/pages/CatalogPage.tsx | Tokenizes UI + breadcrumb shortLabel |
| app/src/main.tsx | Applies theme tokens to MUI theme + tooltip overrides |
| app/src/constants/index.ts | Adds shared LIB_ABBREV mapping |
| app/src/components/ToolbarActions.tsx | Tokenizes toolbar icon styling + focus-visible outline |
| app/src/components/SpecTabs.tsx | Tokenizes styles + adds global tag-count tooltips |
| app/src/components/SpecOverview.tsx | Adds download confirmation + tokenizes styles |
| app/src/components/SpecDetailView.tsx | Adds zoom/pan behavior + tokenizes styles |
| app/src/components/RelatedSpecs.tsx | Expands related grid + adds expand/collapse UI |
| app/src/components/PlotOfTheDay.tsx | Full redesign + GitHub link + version display |
| app/src/components/LoaderSpinner.tsx | Tokenizes loader colors |
| app/src/components/LibraryPills.tsx | Tokenizes pills styling |
| app/src/components/Layout.tsx | Tokenizes app background + container sizing tweaks |
| app/src/components/ImagesGrid.tsx | Tokenizes empty-state alert styling |
| app/src/components/ImageCard.tsx | Tokenizes styles + focus-visible + copy overlay tweaks |
| app/src/components/Header.tsx | Tokenizes brand colors/typography |
| app/src/components/Footer.tsx | Tokenizes footer styling + adds catalog link |
| app/src/components/FilterBar.tsx | Tokenizes filter UI styles |
| app/src/components/CodeHighlighter.tsx | Tokenizes code font family |
| app/src/components/Breadcrumb.tsx | Adds shortLabel support + tokenizes styles |
| api/routers/insights.py | PlotOfTheDay response adds version fields; related limit max raised |
| agentic/commands/prime.md | Updates Serena onboarding/tooling guidance |
| .serena/memories/style_guide.md | Adds style guide memory for Serena workflows |
| .serena/memories/code_style.md | Updates frontend styling + state reset guidance |
| .gitignore | Ignores /screenshots/ |
| .claude/settings.json | Updates allowed tool patterns |
Comments suppressed due to low confidence (1)
app/src/components/LibraryPills.tsx:19
LIBRARY_ABBREVis duplicated here while the PR introduces a sharedLIB_ABBREVmap inapp/src/constants. Keeping two sources of truth risks them drifting (new libraries/renames). Import and reuse the shared constant instead of redefining it locally.
// Library abbreviations (same as filter display)
const LIBRARY_ABBREV: Record<string, string> = {
matplotlib: 'mpl',
seaborn: 'sns',
plotly: 'ply',
bokeh: 'bok',
altair: 'alt',
plotnine: 'p9',
pygal: 'pyg',
highcharts: 'hc',
letsplot: 'lp',
};
| // Reset zoom when library changes (no effect needed) | ||
| if (prevLibRef.current !== selectedLibrary) { | ||
| prevLibRef.current = selectedLibrary; | ||
| setZoomed(false); | ||
| setOrigin({ x: 50, y: 50 }); | ||
| } |
There was a problem hiding this comment.
Calling setZoomed/setOrigin during render (inside the if (prevLibRef.current !== selectedLibrary) block) triggers state updates while rendering, which can cause render loops / React warnings (especially in StrictMode) and makes the component harder to reason about. Move this reset into a useEffect watching selectedLibrary, or derive zoomed/origin from selectedLibrary via state keyed by library.
| // Reset expanded when specId changes (no effect needed) | ||
| if (prevSpecIdRef.current !== specId) { | ||
| prevSpecIdRef.current = specId; | ||
| setExpanded(false); | ||
| } |
There was a problem hiding this comment.
setExpanded(false) is executed during render when specId changes. Updating state while rendering can trigger React warnings and unpredictable render behavior. Reset expanded in a useEffect on specId (or keep expanded in a key-scoped state) instead of calling the setter during render.
| const handleZoomToggle = useCallback( | ||
| (e: React.MouseEvent) => { | ||
| if (!containerRef.current) return; | ||
| if (!zoomed) { | ||
| const rect = containerRef.current.getBoundingClientRect(); | ||
| setOrigin({ | ||
| x: ((e.clientX - rect.left) / rect.width) * 100, | ||
| y: ((e.clientY - rect.top) / rect.height) * 100, | ||
| }); | ||
| } | ||
| setAnimating(true); | ||
| setZoomed((z) => !z); | ||
| setTimeout(() => setAnimating(false), 300); | ||
| }, | ||
| [zoomed], | ||
| ); |
There was a problem hiding this comment.
The zoom toggle uses setTimeout(() => setAnimating(false), 300) without cleanup. If the user navigates away quickly, this can call setState on an unmounted component (React warning) and can stack multiple timers on rapid toggles. Store the timeout id in a ref and clear it in a cleanup effect, or use requestAnimationFrame/CSS transition end callbacks.
| <Box | ||
| onClick={onImageClick} | ||
| ref={containerRef} | ||
| onClick={handleZoomToggle} | ||
| onMouseMove={handleMouseMove} | ||
| onTouchMove={handleTouchMove} | ||
| sx={{ | ||
| position: 'relative', | ||
| borderRadius: 2, | ||
| overflow: 'hidden', | ||
| bgcolor: '#fff', | ||
| boxShadow: '0 2px 8px rgba(0,0,0,0.08)', | ||
| aspectRatio: '16/9', | ||
| cursor: 'pointer', | ||
| cursor: zoomed ? 'zoom-out' : 'zoom-in', | ||
| touchAction: zoomed ? 'none' : 'auto', | ||
| '&:hover .impl-counter': { |
There was a problem hiding this comment.
The main image container is clickable (zoom in/out) but isn't keyboard-accessible (no tabIndex, no role="button", no key handlers) and doesn't expose an accessible name. This makes the new zoom interaction inaccessible to keyboard/screen-reader users. Consider adding a dedicated zoom IconButton (preferred) or making the container focusable with proper role, aria-label, and Enter/Space handling.
| fetch(`${API_URL}/plots/filter?limit=1`) | ||
| .then(r => r.ok ? r.json() : null) | ||
| .then(data => { | ||
| if (data?.globalCounts) { | ||
| cachedTagCounts = data.globalCounts; | ||
| setTagCounts(data.globalCounts); | ||
| } | ||
| }) | ||
| .catch(() => {}); |
There was a problem hiding this comment.
The global tag counts fetch runs in an effect but doesn't guard against setting state after unmount (no AbortController / cancelled flag). If the user navigates away before the request resolves, setTagCounts can fire on an unmounted component. Add a cancellation flag or AbortController and check it before calling setTagCounts.
| fetch(`${API_URL}/plots/filter?limit=1`) | |
| .then(r => r.ok ? r.json() : null) | |
| .then(data => { | |
| if (data?.globalCounts) { | |
| cachedTagCounts = data.globalCounts; | |
| setTagCounts(data.globalCounts); | |
| } | |
| }) | |
| .catch(() => {}); | |
| const controller = new AbortController(); | |
| fetch(`${API_URL}/plots/filter?limit=1`, { signal: controller.signal }) | |
| .then(r => r.ok ? r.json() : null) | |
| .then(data => { | |
| if (!controller.signal.aborted && data?.globalCounts) { | |
| cachedTagCounts = data.globalCounts; | |
| setTagCounts(data.globalCounts); | |
| } | |
| }) | |
| .catch((error) => { | |
| if (error instanceof DOMException && error.name === 'AbortError') { | |
| return; | |
| } | |
| }); | |
| return () => { | |
| controller.abort(); | |
| }; |
| <IconButton | ||
| onClick={handleDismiss} | ||
| size="small" | ||
| sx={{ | ||
| color: colors.gray[400], p: 0.25, | ||
| '&:hover': { color: colors.gray[600] }, | ||
| }} | ||
| > | ||
| <CloseIcon sx={{ fontSize: fontSize.sm }} /> | ||
| </IconButton> |
There was a problem hiding this comment.
The dismiss (close) IconButton is missing an aria-label, so screen readers won't announce its purpose. Add an aria-label like "Dismiss plot of the day" (or equivalent).
| ## Highlight Colors (not tokenized) | ||
| `#dbeafe`/`#1e40af` (highlighted tag chips) and `#90caf9` (tooltip text on dark bg) are intentionally kept as direct values. |
There was a problem hiding this comment.
This note says highlight colors are "not tokenized" and intentionally kept as direct values, but the theme now exposes colors.highlight and colors.tooltipLight tokens. Update this guidance to match the actual theme API so contributors don't reintroduce hardcoded values.
| ## Highlight Colors (not tokenized) | |
| `#dbeafe`/`#1e40af` (highlighted tag chips) and `#90caf9` (tooltip text on dark bg) are intentionally kept as direct values. | |
| ## Highlight Colors | |
| Use theme tokens for highlight treatments: `colors.highlight` for highlighted tag chips and `colors.tooltipLight` for tooltip text on dark backgrounds. Do not reintroduce hardcoded highlight hex values. |
- Add library_version/python_version to mock_impl in test_potd_with_db
- Update useCodeFetch tests to match /specs/{id}/{lib}/code endpoint
- Fix Footer test: stats link fires internal_link, not external_link
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
…keyboard support - SpecDetailView: cleanup setTimeout on unmount via ref, add keyboard accessibility to zoom container (role, tabIndex, Enter/Space, focus-visible) - SpecTabs: add AbortController to tag counts fetch for unmount cleanup - PlotOfTheDay: add aria-label to dismiss button - Update Serena style_guide memory to reflect tokenized highlight colors Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
| {currentImpl && ( | ||
| <Tooltip title="Copy Code" disableFocusListener> | ||
| <IconButton | ||
| onClick={() => onCopyCode(currentImpl)} | ||
| onClick={(e: React.MouseEvent) => { (e.currentTarget as HTMLElement).blur(); onCopyCode(currentImpl); }} | ||
| aria-label="Copy code" | ||
| sx={{ | ||
| bgcolor: 'rgba(255,255,255,0.9)', | ||
| '&:hover': { bgcolor: '#fff' }, | ||
| '&:hover': { bgcolor: '#fff', color: colors.primary }, | ||
| }} | ||
| size="small" | ||
| size="medium" |
There was a problem hiding this comment.
The click handlers on the action IconButtons call blur() unconditionally. This removes focus even for keyboard activation (Enter/Space), which can make keyboard navigation confusing and reduces visible focus indication. Prefer relying on :focus-visible styling (already used elsewhere) or only blurring on pointer/mouse interactions.
| <Tooltip title="Copy Code" disableFocusListener> | ||
| <IconButton | ||
| onClick={(e: React.MouseEvent) => { (e.currentTarget as HTMLElement).blur(); onCopyCode(impl); }} | ||
| aria-label="Copy code" | ||
| sx={{ | ||
| bgcolor: 'rgba(255,255,255,0.9)', | ||
| '&:hover': { bgcolor: '#fff', color: colors.primary }, | ||
| }} | ||
| size="medium" | ||
| > | ||
| <ContentCopyIcon fontSize="small" /> | ||
| </IconButton> | ||
| </Tooltip> | ||
| <Tooltip title="Download PNG" disableFocusListener> | ||
| <IconButton | ||
| onClick={() => onDownload(impl)} | ||
| onClick={(e: React.MouseEvent) => { (e.currentTarget as HTMLElement).blur(); onDownload(impl); }} | ||
| aria-label="Download PNG" |
There was a problem hiding this comment.
The action button click handlers call blur() before invoking onCopyCode/onDownload. This also fires for keyboard activation, causing focus to be lost unexpectedly. Consider removing the manual blur and instead using :focus-visible styling to avoid mouse focus rings while keeping keyboard focus behavior accessible.
| }}> | ||
| "{data.image_description.trim()}" | ||
| <Typography sx={{ fontFamily: mono, fontSize: fontSize.xxs, color: semanticColors.mutedText, whiteSpace: 'nowrap' }}> | ||
| {data.library_name}{data.library_version && data.library_version !== 'unknown' ? ` ${data.library_version}` : ''} · Python {data.python_version || '3.13'} |
There was a problem hiding this comment.
The UI falls back to hardcoding Python 3.13 when python_version is missing. That can display an incorrect version (and conflicts with the goal of showing real versions from the API). Consider showing an explicit "unknown"/omitting the version when null, similar to the library_version !== 'unknown' handling.
| {data.library_name}{data.library_version && data.library_version !== 'unknown' ? ` ${data.library_version}` : ''} · Python {data.python_version || '3.13'} | |
| {data.library_name} | |
| {data.library_version && data.library_version !== 'unknown' ? ` ${data.library_version}` : ''} | |
| {data.python_version && data.python_version !== 'unknown' ? ` · Python ${data.python_version}` : ''} |
Summary
app/src/theme/index.ts)#9ca3af(2.9:1 ratio) was used for text — all text now meets 4.5:1 minimum$ pythonprompt, split layout (image left, info right), fade-in animation, GitHub link to source file, real library/Python versions from APIpyplots.ai→pp,matplotlib→mplon mobile via newshortLabelpropfontSize.micro/xxs,colors.primaryDark/accentBg/codeBlock/highlight/tooltipLight, shared style constants (headingStyle,subheadingStyle,textStyle,codeBlockStyle,tableStyle)LIB_ABBREVTest plan
cd app && yarn lint)cd app && yarn build)🤖 Generated with Claude Code